Oplev kraften i den nye JavaScript Iterator `scan`-hjælper. Lær, hvordan den revolutionerer strømbehandling, statshåndtering og dataindsamling ud over `reduce`.
JavaScript Iterator `scan`: Den manglende brik i akkumulerende strømbehandling
I det stadigt udviklende landskab af moderne webudvikling er data konge. Vi beskæftiger os konstant med informationsstrømme: brugerhændelser, API-svar i realtid, store datasæt og meget mere. At behandle disse data effektivt og deklarativt er en altafgørende udfordring. I årevis har JavaScript-udviklere stolet på den kraftfulde Array.prototype.reduce-metode til at destillere en matrix ned til en enkelt værdi. Men hvad hvis du har brug for at se rejsen, ikke bare destinationen? Hvad hvis du har brug for at observere hvert mellemtrin i en akkumulering?
Det er her et nyt, kraftfuldt værktøj træder ind på scenen: Iterator scan-hjælperen. Som en del af TC39 Iterator Helpers-forslaget, der i øjeblikket er på Stage 3, er scan sat til at revolutionere den måde, vi håndterer sekventielle og strømbaserede data i JavaScript. Det er den funktionelle, elegante modpart til reduce, der giver hele historikken over en operation.
Denne omfattende guide vil tage dig med på et dybt dyk ned i scan-metoden. Vi vil udforske de problemer, den løser, dens syntaks, dens kraftfulde anvendelsesområder fra simple løbende summer til kompleks statshåndtering, og hvordan den passer ind i det bredere økosystem af moderne, hukommelseseffektiv JavaScript.
Den velkendte udfordring: Grænserne for `reduce`
For virkelig at værdsætte, hvad scan bringer til bordet, lad os først genbesøge et almindeligt scenarie. Forestil dig, at du har en strøm af finansielle transaktioner, og du har brug for at beregne den løbende saldo efter hver transaktion. Dataene kan se sådan ud:
const transactions = [100, -20, 50, -10, 75]; // Indskud og udbetalinger
Hvis du kun ville have den endelige saldo, er Array.prototype.reduce det perfekte værktøj:
const finalBalance = transactions.reduce((balance, transaction) => balance + transaction, 0);
console.log(finalBalance); // Output: 195
Dette er kortfattet og effektivt. Men hvad hvis du har brug for at plotte kontosaldoen over tid på et diagram? Du har brug for saldoen efter hver transaktion: [100, 80, 130, 120, 195]. reduce-metoden skjuler disse mellemliggende trin for os; den giver kun det endelige resultat.
Så hvordan ville vi løse dette traditionelt? Vi ville sandsynligvis falde tilbage på en manuel løkke med en ekstern statsvariabel:
const transactions = [100, -20, 50, -10, 75];
const runningBalances = [];
let currentBalance = 0;
for (const transaction of transactions) {
currentBalance += transaction;
runningBalances.push(currentBalance);
}
console.log(runningBalances); // Output: [100, 80, 130, 120, 195]
Dette virker, men det har flere ulemper:
- Imperativ stil: Det er mindre deklarativt. Vi administrerer manuelt staten (
currentBalance) og resultatindsamlingen (runningBalances). - Statfuld og omfattende: Det kræver styring af muterbare variabler uden for løkken, hvilket kan øge den kognitive belastning og potentialet for fejl i mere komplekse scenarier.
- Ikke komponerbar: Det er ikke en ren, kædelig operation. Det bryder flowet af funktionel metodekædning (som
map,filterosv.).
Dette er præcis det problem, som Iterator scan-hjælperen er designet til at løse med elegance og kraft.
Et nyt paradigme: Iterator Helpers-forslaget
Før vi springer direkte ind i scan, er det vigtigt at forstå den kontekst, den lever i. Iterator Helpers-forslaget sigter mod at gøre iteratorer førsteklasses borgere i JavaScript til databehandling. Iteratorer er et grundlæggende koncept i JavaScript - de er motoren bag for...of-løkker, spread-syntaksen (...) og generatorer.
Forslaget tilføjer en række velkendte, array-lignende metoder direkte på Iterator.prototype, inklusive:
map(mapperFn): Transformerer hvert element i iteratoren.filter(filterFn): Giver kun de elementer, der består en test.take(limit): Giver de første N elementer.drop(limit): Springer de første N elementer over.flatMap(mapperFn): Mapper hvert element til en iterator og flader resultatet ud.reduce(reducer, initialValue): Reducerer iteratoren til en enkelt værdi.- Og selvfølgelig,
scan(reducer, initialValue).
Den vigtigste fordel her er doven evaluering. I modsætning til array-metoder, der ofte opretter nye, mellemliggende arrays i hukommelsen, behandler iterator-hjælpere elementer et ad gangen, efter behov. Dette gør dem utroligt hukommelseseffektive til håndtering af meget store eller endda uendelige datastrømme.
Et dybt dyk ned i scan-metoden
scan-metoden er konceptuelt lig reduce, men i stedet for at returnere en enkelt endelig værdi, returnerer den en ny iterator, der giver resultatet af reducer-funktionen ved hvert trin. Det giver dig mulighed for at se hele historikken over akkumuleringen.
Syntaks og parametre
Metodesignaturen er ligetil og vil føles bekendt for alle, der har brugt reduce.
iterator.scan(reducer [, initialValue])
reducer(accumulator, element, index): En funktion, der kaldes for hvert element i iteratoren. Den modtager:accumulator: Den værdi, der returneres af den foregående påkaldelse af reduceren, ellerinitialValue, hvis den leveres.element: Det aktuelle element, der behandles fra kildeiteratoren.index: Indekset for det aktuelle element.
accumulatorfor det næste kald og er også den værdi, somscangiver.initialValue(valgfrit): En initial værdi, der skal bruges som den førsteaccumulator. Hvis den ikke er angivet, bruges det første element i iteratoren som den indledende værdi, og iterationen starter fra det andet element.
Sådan fungerer det: Trin for trin
Lad os spore vores løbende saldoksempel for at se scan i aktion. Husk, at scan opererer på iteratorer, så først skal vi hente en iterator fra vores array.
const transactions = [100, -20, 50, -10, 75];
const initialBalance = 0;
// 1. Få en iterator fra arrayet
const transactionIterator = transactions.values();
// 2. Anvend scan-metoden
const runningBalanceIterator = transactionIterator.scan(
(balance, transaction) => balance + transaction,
initialBalance
);
// 3. Resultatet er en ny iterator. Vi kan konvertere den til en array for at se resultaterne.
const runningBalances = [...runningBalanceIterator];
console.log(runningBalances); // Output: [100, 80, 130, 120, 195]
Her er, hvad der sker under motorhjelmen:
scankaldes med en reducer(a, b) => a + bog eninitialValuepå0.- Iteration 1: Reduceren kaldes med
accumulator = 0(den indledende værdi) ogelement = 100. Den returnerer100.scangiver100. - Iteration 2: Reduceren kaldes med
accumulator = 100(det forrige resultat) ogelement = -20. Den returnerer80.scangiver80. - Iteration 3: Reduceren kaldes med
accumulator = 80ogelement = 50. Den returnerer130.scangiver130. - Iteration 4: Reduceren kaldes med
accumulator = 130ogelement = -10. Den returnerer120.scangiver120. - Iteration 5: Reduceren kaldes med
accumulator = 120ogelement = 75. Den returnerer195.scangiver195.
Resultatet er en ren, deklarativ og komponerbar måde at opnå præcis det, vi havde brug for, uden manuelle løkker eller ekstern statshåndtering.
Praktiske eksempler og globale anvendelsesområder
Kraften i scan rækker langt ud over simple løbende summer. Det er en grundlæggende primitiv for strømbehandling, der kan anvendes på en lang række domæner, der er relevante for udviklere over hele verden.
Eksempel 1: Statshåndtering og hændelseskilde
En af de mest kraftfulde anvendelser af scan er i statshåndtering, der afspejler mønstre, der findes i biblioteker som Redux. Forestil dig, at du har en strøm af brugerhandlinger eller applikationshændelser. Du kan bruge scan til at behandle disse hændelser og producere din applikations tilstand på ethvert tidspunkt.
Lad os modellere en simpel tæller med inkrement-, dekrement- og nulstilhandlinger.
// En generatorfunktion til at simulere en strøm af handlinger
function* actionStream() {
yield { type: 'INCREMENT' };
yield { type: 'INCREMENT' };
yield { type: 'DECREMENT', payload: 2 };
yield { type: 'UNKNOWN_ACTION' }; // Bør ignoreres
yield { type: 'RESET' };
yield { type: 'INCREMENT', payload: 5 };
}
// Den oprindelige tilstand for vores applikation
const initialState = { count: 0 };
// Reducerfunktionen definerer, hvordan tilstanden ændres som reaktion på handlinger
function stateReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + (action.payload || 1) };
case 'DECREMENT':
return { ...state, count: state.count - (action.payload || 1) };
case 'RESET':
return { count: 0 };
default:
return state; // VIGTIGT: Returner altid den aktuelle tilstand for ikke-håndterede handlinger
}
}
// Brug scan til at oprette en iterator over applikationens statshistorik
const stateHistoryIterator = actionStream().scan(stateReducer, initialState);
// Log hver tilstandsændring, som den sker
for (const state of stateHistoryIterator) {
console.log(state);
}
/*
Output:
{ count: 1 }
{ count: 2 }
{ count: 0 }
{ count: 0 } // a.k.a staten var uændret af UNKNOWN_ACTION
{ count: 0 } // efter RESET
{ count: 5 }
*/
Dette er utroligt kraftfuldt. Vi har deklarativt defineret, hvordan vores tilstand udvikler sig og brugt scan til at oprette en komplet, observerbar historik over den tilstand. Dette mønster er fundamentalt for tidsrejsefejlfinding, logning og opbygning af forudsigelige applikationer.
Eksempel 2: Dataindsamling på store strømme
Forestil dig, at du behandler en massiv logfil eller en datastrøm fra IoT-sensorer, der er for stor til at passe ind i hukommelsen. Iterator-hjælpere skinner her. Lad os bruge scan til at spore den maksimale værdi, der er set indtil videre i en strøm af tal.
// En generator til at simulere en meget stor strøm af sensoraflæsninger
function* getSensorReadings() {
yield 22.5;
yield 24.1;
yield 23.8;
yield 28.3; // Ny maks.
yield 27.9;
yield 30.1; // Ny maks.
// ... kunne give millioner mere
}
const readingsIterator = getSensorReadings();
// Brug scan til at spore den maksimale aflæsning over tid
const maxReadingHistory = readingsIterator.scan((maxSoFar, currentReading) => {
return Math.max(maxSoFar, currentReading);
});
// Vi behøver ikke at videregive en initialValue her. `scan` vil bruge det første
// element (22.5) som den initiale maks. og starte fra det andet element.
console.log([...maxReadingHistory]);
// Output: [ 24.1, 24.1, 28.3, 28.3, 30.1 ]
Vent, outputtet kan ved første øjekast virke en smule slukket. Da vi ikke angav en indledende værdi, brugte scan det første element (22.5) som den indledende akkumulator og begyndte at give fra resultatet af den første reduktion. For at se historikken inklusive den indledende værdi, kan vi angive den eksplicit, for eksempel med -Uendeligt.
const maxReadingHistoryWithInitial = getSensorReadings().scan(
(maxSoFar, currentReading) => Math.max(maxSoFar, currentReading),
-Infinity
);
console.log([...maxReadingHistoryWithInitial]);
// Output: [ 22.5, 24.1, 24.1, 28.3, 28.3, 30.1 ]
Dette demonstrerer iteratorernes hukommelseseffektivitet. Vi kan behandle en teoretisk uendelig strøm af data og få det løbende maksimum ved hvert trin uden nogensinde at holde mere end én værdi i hukommelsen ad gangen.
Eksempel 3: Kædning med andre hjælpere til kompleks logik
Den sande kraft i Iterator Helpers-forslaget låses op, når du begynder at kæde metoder sammen. Lad os opbygge en mere kompleks pipeline. Forestil dig en strøm af e-handelsbegivenheder. Vi ønsker at beregne den samlede omsætning over tid, men kun fra succesfuldt gennemførte ordrer placeret af VIP-kunder.
function* getECommerceEvents() {
yield { type: 'PAGE_VIEW', user: 'guest' };
yield { type: 'ORDER_PLACED', user: 'user123', amount: 50, isVip: false };
yield { type: 'ORDER_COMPLETED', user: 'user456', amount: 120, isVip: true };
yield { type: 'ORDER_FAILED', user: 'user789', amount: 200, isVip: true };
yield { type: 'ORDER_COMPLETED', user: 'user101', amount: 75, isVip: true };
yield { type: 'PAGE_VIEW', user: 'user456' };
yield { type: 'ORDER_COMPLETED', user: 'user123', amount: 30, isVip: false }; // Ikke VIP
yield { type: 'ORDER_COMPLETED', user: 'user999', amount: 250, isVip: true };
}
const revenueHistory = getECommerceEvents()
// 1. Filtrer efter de rigtige begivenheder
.filter(event => event.type === 'ORDER_COMPLETED' && event.isVip)
// 2. Kortlæg til bare ordrebeløbet
.map(event => event.amount)
// 3. Scan for at få den løbende sum
.scan((total, amount) => total + amount, 0);
console.log([...revenueHistory]);
// Lad os spore datastrømmen:
// - Efter filter: { amount: 120 }, { amount: 75 }, { amount: 250 }
// - Efter map: 120, 75, 250
// - Efter scan (udbytteværdier):
// - 0 + 120 = 120
// - 120 + 75 = 195
// - 195 + 250 = 445
// Endeligt output: [ 120, 195, 445 ]
Dette eksempel er en smuk demonstration af deklarativ programmering. Koden læses som en beskrivelse af forretningslogikken: filtrer efter gennemførte VIP-ordrer, udtræk beløbet, og beregn derefter den løbende sum. Hvert trin er en lille, genanvendelig og testbar del af en større, hukommelseseffektiv pipeline.
`scan()` vs. `reduce()`: En klar sondring
Det er afgørende at konsolidere forskellen mellem disse to kraftfulde metoder. Selvom de deler en reducer-funktion, er deres formål og output fundamentalt forskellige.
reduce()handler om sammenfatning. Den behandler en hel sekvens for at producere en enkelt, endelig værdi. Rejsen er skjult.scan()handler om transformation og observation. Den behandler en sekvens og producerer en ny sekvens af samme længde, der viser den akkumulerede tilstand ved hvert trin. Rejsen er resultatet.
Her er en side-om-side-sammenligning:
| Funktion | iterator.reduce(reducer, initial) |
iterator.scan(reducer, initial) |
|---|---|---|
| Primært mål | At destillere en sekvens ned til en enkelt opsummeringsværdi. | At observere den akkumulerede værdi i hvert trin i en sekvens. |
| Returværdi | En enkelt værdi (Promise, hvis asynkron) af det endelige akkumulerede resultat. | En ny iterator, der giver hvert mellemakkumuleret resultat. |
| Almindelig analogi | Beregning af den endelige saldo på en bankkonto. | Generering af en bankopgørelse, der viser saldoen efter hver transaktion. |
| Anvendelsessag | At summere tal, finde et maksimum, sammenkæde strenge. | Løbende summer, statshåndtering, beregning af glidende gennemsnit, observation af historiske data. |
Kode sammenligning
const numbers = [1, 2, 3, 4].values(); // Få en iterator
// Reducer: Destinationen
const sum = numbers.reduce((acc, val) => acc + val, 0);
console.log(sum); // Output: 10
// Du har brug for en ny iterator til den næste operation
const numbers2 = [1, 2, 3, 4].values();
// Scan: Rejsen
const runningSum = numbers2.scan((acc, val) => acc + val, 0);
console.log([...runningSum]); // Output: [1, 3, 6, 10]
Sådan bruges Iterator-hjælpere i dag
Fra denne skrivning er Iterator Helpers-forslaget på Stage 3 i TC39-processen. Det betyder, at det er meget tæt på at blive færdiggjort og inkluderet i en fremtidig version af ECMAScript-standarden. Selvom det muligvis ikke er tilgængeligt i alle browsere eller Node.js-miljøer endnu, behøver du ikke at vente med at begynde at bruge det.
Du kan bruge disse kraftfulde funktioner i dag gennem polyfills. Den mest almindelige måde er ved at bruge core-js-biblioteket, som er en omfattende polyfill for moderne JavaScript-funktioner.
For at bruge det vil du typisk installere core-js:
npm install core-js
Og derefter importere den specifikke forslagspolyfill på indgangspunktet i din applikation:
import 'core-js/proposals/iterator-helpers';
// Nu kan du bruge .scan() og andre hjælpere!
const result = [1, 2, 3].values()
.map(x => x * 2)
.scan((a, b) => a + b, 0);
console.log([...result]); // [2, 6, 12]
Alternativt, hvis du bruger en transpiler som Babel, kan du konfigurere den til at inkludere de nødvendige polyfills og transformationer til Stage 3-forslag.
Konklusion: Et nyt værktøj til en ny æra af data
JavaScript Iterator scan-hjælperen er mere end bare en praktisk ny metode; den repræsenterer et skift mod en mere funktionel, deklarativ og hukommelseseffektiv måde at håndtere datastrømme på. Den udfylder et kritisk hul, der er tilbage af reduce, hvilket giver udviklere mulighed for ikke kun at nå frem til et endeligt resultat, men også at observere og handle på hele historien om en akkumulering.
Ved at omfavne scan og det bredere Iterator Helpers-forslag kan du skrive kode, der er:
- Mere deklarativ: Din kode vil tydeligere udtrykke hvad du prøver at opnå, snarere end hvordan du opnår det med manuelle løkker.
- Mere komponerbar: Kæd simple, rene operationer sammen for at opbygge komplekse databehandlingspipelines, der er nemme at læse og ræsonnere om.
- Mere hukommelseseffektiv: Udnyt doven evaluering til at behandle massive eller uendelige datasæt uden at overvælde dit systems hukommelse.
Efterhånden som vi fortsætter med at bygge mere dataintensive og reaktive applikationer, vil værktøjer som scan blive uundværlige. Det er en kraftfuld primitiv, der gør det muligt at implementere sofistikerede mønstre som hændelseskilde og strømbehandling indfødt, elegant og effektivt. Begynd at udforske det i dag, og du vil være godt forberedt til fremtiden for datahåndtering i JavaScript.